#include "ZHttpClient.h"

#include "ZThread.h"
#include "ZApplication.h"

#include <assert.h>
#include <errno.h>
#include <queue>
#include "curl/curl.h"

namespace ZuiLib {

static ZHttpClient *s_pHttpClient = NULL; // pointer to singleton

static char s_errorBuffer[CURL_ERROR_SIZE] = {0};

typedef size_t (*write_callback)(void *ptr, size_t size, size_t nmemb, void *stream);

static std::string s_cookieFilename = "";

// Callback function used by libcurl for collect response data
static size_t writeData(void *ptr, size_t size, size_t nmemb, void *stream)
{
    std::vector<char> *recvBuffer = (std::vector<char>*)stream;
    size_t sizes = size * nmemb;
    
    // add data to the end of recvBuffer
    // write data maybe called more than once in a single request
    recvBuffer->insert(recvBuffer->end(), (char*)ptr, (char*)ptr+sizes);
    
    return sizes;
}

// Callback function used by libcurl for collect header data
static size_t writeHeaderData(void *ptr, size_t size, size_t nmemb, void *stream)
{
    std::vector<char> *recvBuffer = (std::vector<char>*)stream;
    size_t sizes = size * nmemb;
    
    // add data to the end of recvBuffer
    // write data maybe called more than once in a single request
    recvBuffer->insert(recvBuffer->end(), (char*)ptr, (char*)ptr+sizes);
    
    return sizes;
}


static int processGetTask(ZHttpRequest *request, write_callback callback, void *stream, long *errorCode, write_callback headerCallback, void *headerStream, char *errorBuffer);
static int processPostTask(ZHttpRequest *request, write_callback callback, void *stream, long *errorCode, write_callback headerCallback, void *headerStream, char *errorBuffer);
static int processPutTask(ZHttpRequest *request, write_callback callback, void *stream, long *errorCode, write_callback headerCallback, void *headerStream, char *errorBuffer);
static int processDeleteTask(ZHttpRequest *request, write_callback callback, void *stream, long *errorCode, write_callback headerCallback, void *headerStream, char *errorBuffer);
// int processDownloadTask(HttpRequest *task, write_callback callback, void *stream, int32_t *errorCode);
static void processResponse(ZHttpResponse* response, char* errorBuffer);


//Configure curl's timeout property
static bool configureCURL(CURL *handle, char *errorBuffer)
{
    if (!handle) {
        return false;
    }
    
    int32_t code;
    code = curl_easy_setopt(handle, CURLOPT_ERRORBUFFER, errorBuffer);
    if (code != CURLE_OK) {
        return false;
    }
    code = curl_easy_setopt(handle, CURLOPT_TIMEOUT, ZHttpClient::getInstance()->getTimeoutForRead());
    if (code != CURLE_OK) {
        return false;
    }
    code = curl_easy_setopt(handle, CURLOPT_CONNECTTIMEOUT, ZHttpClient::getInstance()->getTimeoutForConnect());
    if (code != CURLE_OK) {
        return false;
    }
    curl_easy_setopt(handle, CURLOPT_SSL_VERIFYPEER, 0L);
    curl_easy_setopt(handle, CURLOPT_SSL_VERIFYHOST, 0L);
    
    // FIXED #3224: The subthread of CCHttpClient interrupts main thread if timeout comes.
    // Document is here: http://curl.haxx.se/libcurl/c/curl_easy_setopt.html#CURLOPTNOSIGNAL 
    curl_easy_setopt(handle, CURLOPT_NOSIGNAL, 1L);
	curl_easy_setopt(handle, CURLOPT_FOLLOWLOCATION, 1L);
    return true;
}

class CURLRaii
{
    /// Instance of CURL
    CURL *_curl;
    /// Keeps custom header data
    curl_slist *_headers;
public:
    CURLRaii()
        : _curl(curl_easy_init())
        , _headers(NULL)
    {
    }

    ~CURLRaii()
    {
        if (_curl)
            curl_easy_cleanup(_curl);
        /* free the linked list for header data */
        if (_headers)
            curl_slist_free_all(_headers);
    }

    template <class T>
    bool setOption(CURLoption option, T data)
    {
        return CURLE_OK == curl_easy_setopt(_curl, option, data);
    }

    /**
     * @brief Inits CURL instance for common usage
     * @param request Null not allowed
     * @param callback Response write callback
     * @param stream Response write stream
     */
    bool init(ZHttpRequest *request, write_callback callback, void *stream, write_callback headerCallback, void *headerStream, char *errorBuffer)
    {
        if (!_curl)
            return false;
        if (!configureCURL(_curl, errorBuffer))
            return false;

        /* get custom header data (if set) */
        std::vector<std::string> headers=request->getHeaders();
        if(!headers.empty())
        {
            /* append custom headers one by one */
            for (std::vector<std::string>::iterator it = headers.begin(); it != headers.end(); ++it)
                _headers = curl_slist_append(_headers,it->c_str());
            /* set custom headers for curl */
            if (!setOption(CURLOPT_HTTPHEADER, _headers))
                return false;
        }
        if (!s_cookieFilename.empty()) {
            if (!setOption(CURLOPT_COOKIEFILE, s_cookieFilename.c_str())) {
                return false;
            }
            if (!setOption(CURLOPT_COOKIEJAR, s_cookieFilename.c_str())) {
                return false;
            }
        }

        return setOption(CURLOPT_URL, request->getUrl())
                && setOption(CURLOPT_WRITEFUNCTION, callback)
                && setOption(CURLOPT_WRITEDATA, stream)
                && setOption(CURLOPT_HEADERFUNCTION, headerCallback)
                && setOption(CURLOPT_HEADERDATA, headerStream);
        
    }

    /// @param responseCode Null not allowed
    bool perform(long *responseCode)
    {
        if (CURLE_OK != curl_easy_perform(_curl))
            return false;
        CURLcode code = curl_easy_getinfo(_curl, CURLINFO_RESPONSE_CODE, responseCode);
        if (code != CURLE_OK || !(*responseCode >= 200 && *responseCode < 300)) {
            printf("Curl curl_easy_getinfo failed: %s", curl_easy_strerror(code));
            return false;
        }
        // Get some mor data.
        
        return true;
    }
};

//Process Get Request
static int processGetTask(ZHttpRequest *request, write_callback callback, void *stream, long *responseCode, write_callback headerCallback, void *headerStream, char *errorBuffer)
{
    CURLRaii curl;
    bool ok = curl.init(request, callback, stream, headerCallback, headerStream, errorBuffer)
            && curl.setOption(CURLOPT_FOLLOWLOCATION, true)
            && curl.perform(responseCode);
    return ok ? 0 : 1;
}

//Process POST Request
static int processPostTask(ZHttpRequest *request, write_callback callback, void *stream, long *responseCode, write_callback headerCallback, void *headerStream, char *errorBuffer)
{
    CURLRaii curl;
    bool ok = curl.init(request, callback, stream, headerCallback, headerStream, errorBuffer)
            && curl.setOption(CURLOPT_POST, 1)
            && curl.setOption(CURLOPT_POSTFIELDS, request->getRequestData())
            && curl.setOption(CURLOPT_POSTFIELDSIZE, request->getRequestDataSize())
            && curl.perform(responseCode);
    return ok ? 0 : 1;
}

//Process PUT Request
static int processPutTask(ZHttpRequest *request, write_callback callback, void *stream, long *responseCode, write_callback headerCallback, void *headerStream, char *errorBuffer)
{
    CURLRaii curl;
    bool ok = curl.init(request, callback, stream, headerCallback, headerStream, errorBuffer)
            && curl.setOption(CURLOPT_CUSTOMREQUEST, "PUT")
            && curl.setOption(CURLOPT_POSTFIELDS, request->getRequestData())
            && curl.setOption(CURLOPT_POSTFIELDSIZE, request->getRequestDataSize())
            && curl.perform(responseCode);
    return ok ? 0 : 1;
}

//Process DELETE Request
static int processDeleteTask(ZHttpRequest *request, write_callback callback, void *stream, long *responseCode, write_callback headerCallback, void *headerStream, char *errorBuffer)
{
    CURLRaii curl;
    bool ok = curl.init(request, callback, stream, headerCallback, headerStream, errorBuffer)
            && curl.setOption(CURLOPT_CUSTOMREQUEST, "DELETE")
            && curl.setOption(CURLOPT_FOLLOWLOCATION, true)
            && curl.perform(responseCode);
    return ok ? 0 : 1;
}


// Process Response
static void processResponse(ZHttpResponse* response, char* errorBuffer)
{
    auto request = response->getHttpRequest();
    long responseCode = -1;
    int retValue = 0;

    // Process the request -> get response packet
    switch (request->getRequestType())
    {
    case ZHttpRequest::TYPE_GET: // HTTP GET
        retValue = processGetTask(request,
            writeData, 
            response->getResponseData(), 
            &responseCode,
            writeHeaderData,
            response->getResponseHeader(),
            errorBuffer);
        break;

    case ZHttpRequest::TYPE_POST: // HTTP POST
        retValue = processPostTask(request,
            writeData, 
            response->getResponseData(), 
            &responseCode,
            writeHeaderData,
            response->getResponseHeader(),
            errorBuffer);
        break;

    case ZHttpRequest::TYPE_PUT:
        retValue = processPutTask(request,
            writeData,
            response->getResponseData(),
            &responseCode,
            writeHeaderData,
            response->getResponseHeader(),
            errorBuffer);
        break;

    case ZHttpRequest::TYPE_DELETE:
        retValue = processDeleteTask(request,
            writeData,
            response->getResponseData(),
            &responseCode,
            writeHeaderData,
            response->getResponseHeader(),
            errorBuffer);
        break;

    default:
        assert(false && "CCHttpClient: unkown request type, only GET and POSt are supported");
        break;
    }

    // write data to HttpResponse
    response->setResponseCode(responseCode);

    if (retValue != 0) 
    {
        response->setSucceed(false);
        response->setErrorBuffer(errorBuffer);
    }
    else
    {
        response->setSucceed(true);
    }
}

// HttpClient implementation
ZHttpClient* ZHttpClient::getInstance()
{
	if (s_pHttpClient == NULL) {
		s_pHttpClient = new ZHttpClient();
	}
	return s_pHttpClient;
}

void ZHttpClient::destroyInstance()
{
    delete s_pHttpClient;
	s_pHttpClient=NULL;
}

void ZHttpClient::enableCookies(const char* cookieFile) {
    s_cookieFilename = std::string(cookieFile);
}

ZHttpClient::ZHttpClient()
: _timeoutForConnect(30)
, _timeoutForRead(60)
{
	curl_global_init(CURL_GLOBAL_ALL);
}

ZHttpClient::~ZHttpClient()
{
    s_pHttpClient = NULL;
	curl_global_cleanup();
}

class ThreadCallback:public ZThreadCallback
{
public:
	ThreadCallback(ZHttpRequestPtr request)
		:m_request(request)
	{

	}

	class MainThreadScheduler:public ZSchedulerBase
	{
	public:
		MainThreadScheduler(ZHttpResponsePtr responce)
			:m_responce(responce)
		{

		}

		void Run()
		{
			ZHttpRequestPtr request=m_responce->getHttpRequest();
			ZSchedulerBase* pTarget = request->getTarget();
			SEL_HttpResponse pSelector = request->getSelector();

			if (pTarget && pSelector)
			{
			    (pTarget->*pSelector)(s_pHttpClient, m_responce);
			}
		}

	private:
		ZHttpResponsePtr m_responce;
	};


	virtual int OnRun()
	{
		ZHttpResponsePtr response = new ZRefCountedObject<ZHttpResponse>(m_request);
		char errorBuffer[CURL_ERROR_SIZE] = { 0 };
		processResponse(response, errorBuffer);
		ZApplication::Instance()->PostSchedule(new ZRefCountedObject<MainThreadScheduler>(response),schedule_selector_void(MainThreadScheduler::Run));
		return 0;
	}

private:
	ZHttpRequestPtr m_request;
};



//Add a get task to queue
void ZHttpClient::send(ZHttpRequestPtr request)
{
	if (request)
		ZThreadPool::Instance()->PostRunable(new ZRefCountedObject<ThreadCallback>(request));
}



void ZHttpClient::sendImmediate(ZHttpRequestPtr request)
{
	if(request)
	{
		ZRefCountedPtr<ZThread> thread=ZThread::CreateThread();
		thread->Start(new ZRefCountedObject<ThreadCallback>(request));
	}
}


}


